fix(spec): retire page.assignedProfiles and answer profiles: with the permission-set route - #17835
Conversation
…e false records Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH
…oute Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH
…ur locale bundles Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH
Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH
…signedprofiles-removal # Conflicts: # packages/spec/src/migrations/registry.ts
Discharges the os-regen deferral the merge commit recorded. Restores the `ui/ObjectKanbanProps:quickAdd [RETIRED]` baseline marker the textual merge dropped, and fixes the rationale concatenation where both sides appended a paragraph to step18. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH
📓 Docs Drift CheckThis PR changes 3 package(s): 8 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 2 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 137 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 513e604db98e84debf834db396900ee87f927daa && git checkout 513e604db98e84debf834db396900ee87f927daa
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin a0dd872c1b01d5afc0ef6eb389d7c874b4a51cfa 4d55c069c409df8ee29ed9aebdf2efaed2ced6f2 && git checkout -B drift-repro a0dd872c1b01d5afc0ef6eb389d7c874b4a51cfa && git merge --no-ff 4d55c069c409df8ee29ed9aebdf2efaed2ced6f2
node scripts/docs-audit/affected-docs.mjs --json a0dd872c1b01d5afc0ef6eb389d7c874b4a51cfa
|
The director seat ruled option B (decision batch 125 item 1, maintainer verbatim 「同意」): the retirement ships under the launch-window convention, and 18.0.0 is cut as a planned act rather than as a side effect of one p2 retirement. Execution clause, quoted: > PR 17835: changeset level `minor`, the `**BREAKING**` banner and the > ADR-0087 entry stay as written; the guard that held the PR is > satisfied by the level; the spec seat finishes review and merges. So this is one token on one line. The `**BREAKING**` banner and the `adr-0087: registered` disposition marker are deliberately untouched: during the launch window the bump level is not the carrier of breaking-ness, those two are, and `check-adr-0087-registration.mjs` still judges this changeset on the banner alone (its verdict line moves from `[major+BREAKING]` to `[BREAKING]`, not to "nothing to look at"). Claude-Session: https://claude.ai/code/session_01EfsizFDgAcEjpwv4oM3WGT Co-authored-by: Claude <noreply@anthropic.com>
…signedprofiles-removal
`os-regen-merge.sh` step 4. The merge driver exited 0 while dropping
main's side of two generated artifacts; regenerating from the merged
tree restores them:
authorable-surface/ui.json + ui/Action:execution
+ ui/CalendarConfig:allDayField
liveness/state-counts.md counts re-derived (total 959 -> 973)
This branch's own deliverable is unaffected: `ui/Page:assignedProfiles
[RETIRED]` is still present, and the `page` row still reads 22 live /
1 dead / 24 total.
Claude-Session: https://claude.ai/code/session_01EfsizFDgAcEjpwv4oM3WGT
Co-authored-by: Claude <noreply@anthropic.com>
Contract review FAIL, three grounds. This commit closes F2 and F3 and the in-file half of F1; the merge commits before it close F1's other half. F1 — `retired-key-migrate-sentence.test.ts` was red on this branch. The semantic entry's `acceptanceCriteria` spelled the pin's marker, `os migrate meta --from 17`, in a backticked code span, and the pin then requires the house sentence anchored at that marker and final in its literal. It is an acceptance criterion, not a tombstone prescription, so the sentence cannot be literal-final there. Dropped the marker instead and named the conversion that does the work. Every other semantic entry in the tree avoids the marker the same way (measured: this was the only `--from <digits>` occurrence under `entries/semantic/`); the sibling `--stored` spelling it keeps does not match the marker. F2 — the tombstone named a version that will not exist. The string names the npm package and `shared/retired-key.ts` defines the field as the version that removed the key; under the launch-window `minor` route this ships as 17.5.0, not 18. The `18` tracked `toMajor`, which is chain bookkeeping, so the sentence conflated two different facts. The sibling that took the same route, `view.pageName`, spells `17.5.0`. The three pre-existing `spec 17` tombstones in this file are NOT touched. F3 — two shipped records described the route this PR did not take. The retired-keys entry said the key is deleted from the shape with the prescription in the `guidance` table; it is a `retiredKey()` tombstone and there is no guidance entry for it. The changeset said the liveness row is deleted three paragraphs after correctly saying it stays as `dead`; the row exists and reads `dead`. Both now describe what is built. Both ship, which is why they were FAIL grounds. `migrations/registry.ts` is regenerated from the corrected entry, not hand-edited. Claude-Session: https://claude.ai/code/session_01EfsizFDgAcEjpwv4oM3WGT Co-authored-by: Claude <noreply@anthropic.com>
`content/docs/references/ui/page.mdx` projects the tombstone prescription verbatim, so the F2 version correction moves one table row. Generated, not hand-edited: `pnpm --filter @objectstack/spec gen:docs` after a full build, and `check:generated` named this as the one stale artifact of 15. Claude-Session: https://claude.ai/code/session_01EfsizFDgAcEjpwv4oM3WGT Co-authored-by: Claude <noreply@anthropic.com>
…signedprofiles-removal # Conflicts: # packages/spec/src/conversions/registry.ts # packages/spec/src/migrations/registry.ts
Discharges the merge commit's os-regen deferral. `migrations/registry.ts` conflicted textually because both sides appended an entry; it is GENERATED from `migrations/entries/`, so it was resolved by taking main's side in the merge and re-deriving it here from the union of both sides' entry files. Both are present afterwards: this branch's `ui/Page:assignedProfiles` (carrying the corrected tombstone prose) and main's four `system/LoggingConfig` / `HttpDestinationConfig` duration-key entries. `conversions/registry.ts` is hand-written and was resolved by hand, both intents kept, main's `listViewSortStringClauseToArray` first and this branch's `pageAssignedProfilesRemoved` after it, in the const block and in the protocol-18 array alike. Nothing of either side was dropped, renamed or reordered. `liveness/state-counts.md` re-derived on the merged tree. Claude-Session: https://claude.ai/code/session_01EfsizFDgAcEjpwv4oM3WGT Co-authored-by: Claude <noreply@anthropic.com>
…n dropped
`migrations/registry.ts` is generated only BETWEEN its `os-generated`
markers; `step18.conversionIds` and `step18.rationale` are hand-authored
append regions outside them. Resolving the merge conflict by taking
main's side and regenerating therefore restored every entries-derived
row and silently dropped both of this branch's hand-edits, which no
generator reproduces.
The consequence was not cosmetic: without
`'page-assigned-profiles-removed'` in `step18.conversionIds` the 17 -> 18
hop stops applying the conversion at all, so a replayed page keeps
`assignedProfiles`. `migrations.test.ts`'s chain-replay composability
gate caught it — "expected { pages: [ {...3}, {...3} ] } to deeply equal
{ pages: [ {...2}, {...3} ] }", the key still present after the chain.
That gate is the reason a clean merge is not a working merge.
Both intents are kept, main's first: the conversion id list carries
`list-view-sort-string-clause-to-array` then
`page-assigned-profiles-removed`, and the rationale carries main's
list-view sort paragraph then this branch's `page.assignedProfiles` one.
Re-running `gen:migration-registry` afterwards is byte-identical, which
is the proof these lines sit outside the generated regions.
Claude-Session: https://claude.ai/code/session_01EfsizFDgAcEjpwv4oM3WGT
Co-authored-by: Claude <noreply@anthropic.com>
|
Patch round closed — CI is green. ⛔ NOT enqueued: one gate remains, and it is not this seat's to clear. PM seat, acting on a maintainer instruction given in live PM chat 2026-09-13, verbatim: 「红的你要帮我解决啊」 (a direct instruction to clear the red CI on the stale open PRs, this one among them). Two patch rounds were dispatched to an What was red, and what fixed it
⭐ The round-1 regression is recorded deliberately: the PR was briefly worse than when it started, and a reviewer reading only the final state would not see that the sentence fix and the type fix are two separate repairs. CI, read on GitHub rather than from the report —
|
Correction to this seat's previous comment (
|
|
Tier: served at CONTRACT_REVIEW_TIER (verified by the seat from this round's transcript) Contract review (re-review of a moved head)Reviewed head 1. The three FAIL grounds — each verified on the treeF1 — closed, both halves.
F2 — closed. F3 — closed, both records. The retired-keys entry no longer claims a strict-shape deletion with a 2. The self-caught defect — restoration verified complete and correct
3. What the two
|
Clause-② provenance — landing
Records: the FAIL on head
Tier: both rounds served at
Pre-landing checks
Path face from ⭐ What the re-review proved that a gate could notThe patch round's own merge resolution had silently dropped The re-review did not take the fix on trust. It ran two ablations:
⇒ the restored lines demonstrably lie outside the generated regions, which is the only thing that makes the fix durable. It further reconciled 95 declared / 95 referenced / 0 orphans, and confirmed This defect class is filed as #18047 (with this evidence transplanted onto it). Generated by Claude Code |
|
Landing state — complete and verified, blocked on one platform action.
All three pre-landing checks pass on head
⭐ That last row re-confirms a platform fact recorded on 2026-09-11: the bare PATCH is accepted and silently ignored. ⛔ Do not read its 200 as a landing. Remaining work is one action: flip ready, then enqueue. Everything upstream of it is done and verified. Generated by Claude Code |
|
Addendum — the platform outage cleared; both PRs are now in their final states.
#17811 is deliberately left for the maintainer's hand. #17189 is
⭐ One platform fact re-confirmed today, worth keeping: a bare Generated by Claude Code |
The step-1 conflict report partitioned the conflicted set against the `merge=os-regen` routing list alone, so `non_regen_conflicts` was the set difference and nothing asked whether a path was generated. A generated path that is deliberately NOT routed — `NOT_DRIVER_MANAGED` in `scripts/regen-artifacts.mjs` — therefore drew the "NON-generated files, resolve those by hand" message together with its "do not resolve generated files textually" line, two sentences about one file with nothing saying which governs. The conflicted set is now partitioned by generatedness first and routing second, into three classes: unrouted and undeclared (today's message, unchanged), routed and MIXED (today's message, unchanged), and declared in `NOT_DRIVER_MANAGED`, which gets a new per-path reading. Class 3 is not one instruction, and the ledger is what shows it: "resolve by regeneration" is right for three of its thirty tracked entries and wrong for the rest, whose own entries say a merge must never recompute them. The discriminator is the generated-region marker pair in the conflicted file, not the entry's `gen` field, which is an accounting field carried by every ratchet in the list. For a marked file the report also answers the caveat instead of delegating it: it reads both sides out of the index, strips the generated regions from each, and says whether the remainders differ — the PR #17835 shape, where taking a side dropped two hand-authored regions silently with every gate green. Claude-Session: https://claude.ai/code/session_01DAcomhvR9kKizeYgg89Vo8 Co-authored-by: Claude <noreply@anthropic.com>
…ray form when they refuse the record form (objectstack-ai#17846) Fixes objectstack-ai#17320 Clause-②: no — the change is to what a refusal **says**. No accept set moves in either direction, no key is added to any published payload, and the generated `json-schema/` + `authorable-surface` artifacts are byte-identical after the change (`git status` clean across two full `@objectstack/spec` builds). `check:api-surface` is green with no regeneration: the helper is module-internal and is not exported from any barrel, exactly as its model `shared/strict-object.ts` is not. ## What was wrong Seven `filter` doors converged on `z.array(ViewFilterRuleSchema)` in the objectui#6206 family. On the record form an author used to write, each produced exactly one zod issue and nothing else — measured on the built artifact before the change, at all seven: ``` [invalid_type] path=["filter"] expected=array message: Invalid input: expected array, received object ``` The prescription was already written down twice, in two places a parse never reaches: every one of the seven `.describe()` strings, and in full in the three `18.*-filter-rule-array` semantic migration entries. Nothing bridges `.describe()` into a zod issue, and this package installs no global error map. Re-verified on this branch's own head with the card's own lit control: `setErrorMap` / `z.config` under `packages/spec/src` → **0** hits; `strictObjectError` (which does exactly this bridging for the unknown-key case) → **14** hits, so the probe reaches. The population at these doors is the authors — human and AI — whose previously-legal metadata the convergence broke, which is when a refusal most needs to name the new spelling. ## The seven doors, located by declaring symbol Each was re-derived from the tree rather than trusted from the card; the card's list is **correct and complete**. The marker that separates a *converged* door from a door that was always an array is the migration pointer in its own `.describe()` — `The MongoDB-style record form is refused — see migration ...` — which occurs exactly 7 times in non-test sources, in exactly these two files: | door | declaring symbol | file | |---|---|---| | the binding every data-bound element carries | `ElementDataSourceSchema.filter` | `packages/spec/src/ui/page.zod.ts` | | `object-grid` | `ObjectGridPropsSchema.filter` | `packages/spec/src/ui/component.zod.ts` | | `object-metric` | `ObjectMetricPropsSchema.filter` | `packages/spec/src/ui/component.zod.ts` | | `object-kanban` | `ObjectKanbanPropsSchema.filter` | `packages/spec/src/ui/component.zod.ts` | | `object-calendar` | `ObjectCalendarPropsSchema.filter` | `packages/spec/src/ui/component.zod.ts` | | `element:number` | `ElementNumberPropsSchema.filter` | `packages/spec/src/ui/component.zod.ts` | | `element:record_picker` | `ElementRecordPickerPropsSchema.filter` | `packages/spec/src/ui/component.zod.ts` | The six `ComponentPropsMap` rows name those schemas, so the card's `ComponentPropsMap['x'].filter` spelling and the symbol spelling are the same door. Four other `z.array(ViewFilterRuleSchema)` keys exist in `ui/` (`RecordRelatedListProps.filter` and its Add-affordance picker, `ViewTabSchema.filter`, `ListViewShapeSchema.filter`, `FormFieldPublicPickerSchema.filter`, `ListPageSchema.filterBy`) — none carries the migration pointer, because none of them ever took the record form. They are out of this card's population and are untouched. ## The shape `shared/strict-object.ts` is the model, for the reason the card gives: guidance **derived from the schema** rather than transcribed beside it, so it cannot drift. A hand-copied sentence at seven sites is what that argues against — and this card's own subject is a prescription that fell out of step with a refusal. New module `packages/spec/src/ui/filter-rule-array.ts` exports one helper, wired through the zod-v4 `{ error }` param at all seven doors. Everything it can derive, it derives: - the rule shape `[{ field, operator, value }, ...]` is read from `ViewFilterRuleSchema`'s own shape (`_zod.def.shape`), on first refusal — never at module load, which would force the `lazySchema` while `view.zod` is still initialising under `OS_EAGER_SCHEMAS=1`, the import-cycle footgun `strictObjectError` already defers around; - the canonical operator is `normalizeFilterOperator('eq')`, the same fold the door itself runs; - the worked rewrite is computed from **the author's own record**, so the example names their fields. What stays per-call is what carries judgement rather than transcription — the same split `strictObject` draws: `surface` and the `migration` id. Both are pinned: the test holds every `migration` equal to a real entry in `MIGRATIONS_BY_MAJOR`, and holds every wired door's `surface` equal to the one its own `strictObject` declaration registered (walked out of `strictObjectDeclarations()`, with a lit control that the walk reached all seven). Fall-through is deliberate and pinned: the map answers only a plain record and returns `undefined` for everything else, as `flattenedViewOverlayFields()` does. A blanket message would overwrite the element-level issues an array author needs, which is the diagnosis this change exists to protect. ## Before / after, per door, on the BUILT artifact Seven separate readings, taken by parsing against `packages/spec/dist/ui/index.mjs` — `pnpm --filter @objectstack/spec build` run to completion (both tsup passes; `check-dts-emitted: 34/34`) before each side. **Before** — every door, exactly one issue: `[invalid_type] path=["filter"] expected=array`, *"Invalid input: expected array, received object"*. **After** — every door, still exactly one issue, still `invalid_type` at `filter`, now saying (this is `object-grid`; the other six differ only in the surface and, for `element:number` / `element:record_picker`, the migration id): > `` `filter` `` on this `object-grid` takes the ViewFilterRule ARRAY form `[{ field, operator, value }, ...]`, and this value is the MongoDB-style record form this door took before the one-filter-orthography convergence. Write one rule per record key — they AND — so this filter becomes `[{ field: 'status', operator: 'equals', value: 'active' }]`. Legacy operator shorthands (`eq`, `gt`, `notIn`, …) are accepted and normalized on parse. Full conversion table: migration `element-data-source-and-object-block-filter-rule-array`. The message **names the new spelling**: the array form, the author's own field lifted into `field`, the canonical `equals`, and the entry id. Per-door surfaces after the change: `this element data source` · ``this `object-grid` `` · ``this `object-metric` `` · ``this `object-kanban` `` · ``this `object-calendar` `` · ``this `element:number` `` · ``this `element:record_picker` ``. Per-door migration ids: the five `element-data-source-and-object-block-filter-rule-array` doors, plus `element-number-filter-rule-array` and `element-record-picker-filter-rule-array`. **Door-shaped negative controls, all seven, after the change** — unchanged from before: - an array with a bad element → one issue at `filter.0.operator`, *"Invalid option: expected one of "equals"|"not_equals"|…"* — zod's own words, no guidance text, and the array door itself says nothing at `filter`; - a string → *"Invalid input: expected array, received string"*; - a valid rule array → accepted at all seven. ## Ablation The pins resolve **`src/`**, not `dist/`: `filter-rule-array-guidance.test.ts` imports `./component.zod`, `./page.zod`, `./view.zod` relatively, and `packages/spec`'s vitest config declares no alias that would route them elsewhere. So **no rebuild leg is needed**, and both legs below changed the verdict from a source-only mutation, which is itself the proof. **Leg A — restore the two door files to their pre-change bytes** (`git checkout BASE -- THE_TWO_DOOR_PATHS`), i.e. the helper exists but nothing is wired: - on-disk proof of the mutation, read first: `ruleArrayFilterError` occurrences `page.zod.ts` 2 → **0**, `component.zod.ts` 7 → **0**; - verdict: **12 failed | 15 passed (27)** — every pin that asserts the new behaviour is red; - the 15 that stayed green are the controls, which is what a control is for: the negative-control cases (§2), the migration-registry pins (§3), and the helper's own unit pin, which leg A cannot reach. **Leg B — neutralise the helper itself** (one injected early `return undefined`): - on-disk proof: marker occurrences 0 → **1**, verified before the run; - verdict: **13 failed | 14 passed (27)** — the same 12 plus the helper unit pin. Both legs restored and **proven restored by `git hash-object` against the HEAD blob**, not assumed from an exit code: leg A `3c4942e3cbfd8071eb372b9dec91dced7682c1d1` / `65eb6d491b12e9879238bafa03c7c127e0e9ae47`, leg B `9b2f92cf38f5d3fc6534f87e6b637825319d100a`, each equal to `git rev-parse HEAD:PATH`, with `git diff HEAD` empty for those paths afterwards. Both scripts carried `trap RESTORE EXIT INT TERM` with absolute paths resolved from `git rev-parse --show-toplevel`, and both treated an empty or mismatched hash as a loud failure. Leg B's first attempt is worth recording: a `perl -0pi` quoting error wrote nothing, the marker count came back **0**, and the guard refused the run rather than reporting a green ablation over an unmutated tree. ## Changeset Measured, not assumed. Both tsup passes confirmed finished before the reading (`check-dts-emitted: @objectstack/spec - 34/34`), then `npm pack --dry-run --json` — 2012 packed files: - **positive control** (the new runtime message text): present in **18** packed files (`dist/*/index.js|.mjs`, browser builds included); - **negative control** (text that exists only in the new test file): present in **0**; - **lit control** (a pre-existing shipped string, same scan, same file list): present in **62** — so the scan reaches. `src/ui/page.zod.ts` and `src/ui/component.zod.ts` are additionally shipped **as source** by `files[]`'s `src/**/*.zod.ts`. The new helper and the new test are not packed. ⇒ published text moves ⇒ `.changeset/17320-filter-rule-array-guidance.md`, `patch`. ## Verification - `pnpm --filter @objectstack/spec test` — **472 files / 13426 tests passed**. - `pnpm --filter @objectstack/spec typecheck` — green (`tsc --noEmit`, `check:scripts-typecheck`, `check:test-typecheck`: the test layer compiles, 54 files / 259 pinned errors held). - `pnpm lint` (`eslint . --no-inline-config`, repo-wide, no narrowing) — green. - Derived gate families: `node scripts/pm/dispatch-gates.mjs --commands` → **82**; all 82 run with the exit code captured before any pipe; `--ran` reconciliation: **82 derived, 82 run, 0 NOT-MEASURED, 0 UNRUN** (a derived zero — every family recorded a code and none is 3). Five needed a second, correct invocation and are reported at their real reading, not their first: `check:doc-formula-expressions`, `check:lean-entry-closure`, `check:dual-build-cjs-loads` and `check:type-check-debt` each exited **3 = PREREQUISITE NOT MET = NOT MEASURED** and were re-run after building the closure they named (the last two after a full `turbo run build` over every workspace package); `check:react-declaration-parity` exited 1 only because `MANIFEST` was unset, and is green run as CI runs it, with the baseline ratchet clean. - Control-byte sweep over the five changed files: **0** hits for `[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]`, with the lit control (same engine, same file list, class widened by one printable byte) hitting 149 / 209 / 840 / 3133 / 48. `pnpm check:nul-bytes` green. - Commit messages swept per token, each counted separately, with a lit control file that hits every one: the eleven relation stems **0** each, `#` + digits **0**, model identifiers **0** (the only matches for a deliberately over-broad model pattern are the two required `Claude-Session:` trailers). Gate sweep derived and run at `cea666718f`; `origin/main` was merged once more afterwards (`cdfd8d142a`, disjoint files) and the pin file re-run green on that head. Merged `origin/main` before opening, as asked — PR objectstack-ai#17835 is parked on `ui/page.zod.ts` and its hunks are untouched. ## 验收备注 Out of scope, noted, not filed — no PR or person is queued to touch these files for these reasons: - Nothing gates the `see migration ...` ids that seven `.describe()` strings already carry; a renamed or deleted entry would strand all seven silently. This change's own `migration` ids are pinned against the registry, so the coupling is checked on the new channel but not on the old one. Carrier: none today. - The second guess the card predicts — an ObjectQL AST tuple array — still lands as a bare `invalid_type` at `filter.0`, raised by `ViewFilterRuleSchema` itself rather than by the array door. Out of this card's population (the array door is the subject), and deliberately left alone so the element-level diagnosis stays zod's. - `objectStackErrorMap` (`shared/error-map.zod.ts`) does exist and does handle `invalid_type` — it is opt-in per parse (`safeParsePretty`), never installed globally. The card's "no global error map" reading is exact as written; this is a note that the package is not entirely without one, in case a future round looks for a home for cross-cutting guidance. --- _Generated by [Claude Code](https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
…ss — a NOT_DRIVER_MANAGED path is resolved by regeneration, not by hand (objectstack-ai#18047) (objectstack-ai#18089) Fixes objectstack-ai#18047 `scripts/pm/os-regen-merge.sh`'s step-1 conflict report partitioned the conflicted set against the `merge=os-regen` routing list alone, so a generated path that is deliberately **not** routed was labelled `NON-generated` and sent to a hand merge — while the third line of the same message forbids resolving a generated file textually. The operator could satisfy neither sentence, and nothing in the output said which governed. ## Premise readings, taken before writing (worktree at `57343f761`) | # | premise | reading | when | |:--|:--|:--|:--| | P1 | the partition keys solely on routing membership; the script references `regen-artifacts.mjs` 0 times | **holds** — `grep -c 'regen-artifacts' scripts/pm/os-regen-merge.sh` ⇒ `0`; `non_regen_conflicts` was the set difference at `:368-374` | 2026-09-14T00:28Z | | P2 | `NOT_DRIVER_MANAGED` is exported as data and names `packages/spec/src/migrations/registry.ts` | **holds, and is not sufficient** — see “the design call” below | 2026-09-14T00:31Z | | P3 | `git check-attr merge` reads `unspecified` for `registry.ts`, `os-regen` for the lit control | **holds** — `packages/spec/src/migrations/registry.ts: merge: unspecified`, `packages/spec/authorable-surface/system.json: merge: os-regen` | 2026-09-14T00:29:43Z | | P4 | no self-test fixture covers a generated + NOT-routed conflict | **holds** — case 6 builds only driver-routed MIXED rows; the word `NOT_DRIVER_MANAGED` did not appear in the file | 2026-09-14T00:30Z |⚠️ The worktree was cut from `origin/main` after a sibling fetch advanced it past the sha in the dispatch: base is `57343f761`, with `7ef05f997` an ancestor of it (`git merge-base --is-ancestor` ⇒ exit 0). ## The design call triage fenced — no new marker was invented, and P2 needed one more reading Triage left open whether the generated-but-unrouted set is machine-readable or stays prose, and fenced it: *「⚠️ If the implementer finds the three-case partition **cannot** be done without inventing that marker, that is a new surface ⇒ report it rather than inventing one silently.」* `NOT_DRIVER_MANAGED` does name `registry.ts`, so membership is readable as data and the script reads it. But membership alone **cannot carry class 3's message**, and that is a reading of the ledger rather than a judgement call: of its 30 tracked entries, *“resolve by regeneration”* is correct for **three** and wrong for the other 27. - `packages/spec/src/migrations/registry.ts`, `skills/README.md` and `content/docs/ai/skills-reference.mdx` are MIXED — a generator owns the text between a marker pair, a human owns everything outside it. The module's own header names exactly these three as the files `NOT_DRIVER_MANAGED` *“turns away”*. One more of that shape (`content/docs/permissions/tenant-audit-census.mdx`) is reached through a directory entry. - the fifteen `test-typecheck-debt.json` ledgers, `docs-import-surface.baseline.json` and their neighbours are **shrink-only ratchets whose own entries say a merge must never recompute them**. `packages/sdui-parser/objectui-lockstep.json` cannot be regenerated here at all (it needs a sibling checkout); the scaffold templates' generator refuses a file it did not already stamp; `packages/spec/src/conversions/registry.ts` and `docs/audits/**` have no generator whatsoever. ⛔ The entry's `gen` field is **not** the discriminator either. It is an accounting field — recorded where the generator appears in no `REGEN_ARTIFACTS` row — so every ratchet above carries one while `docs-import-surface.baseline.json`, which `gen:docs` really does write, carries none. Keying the message on `gen` would send **seventeen** paths to a regeneration their own ledger entry forbids: this card's defect again, one class over. The discriminator used instead is the **generated-region marker pair in the conflicted file**, which is the property the message actually depends on. ⛔ That is not a new marker: both vocabularies are already written by the tree's own generators, and `scripts/check-role-word.mjs` spells the second one once as a consumer and states the rule — *“a rename happens at the generators and arrives here, not the other way round.”* So no new surface was created and nothing was added to `NOT_DRIVER_MANAGED`; the diff is one file.⚠️ **Reported rather than assumed:** class 3 therefore prints **two** per-path readings, not one. The card's suggested wording is class 3's *marked* shape; the *unmarked* shape gets the opposite instruction. A single blanket “resolve by regeneration” for all 30 entries would have been a new unobeyable instruction of exactly the reported kind. ## The three printed cases Classes 1 and 2 are **byte-for-byte unchanged** and, when no class-3 path is present, the branch they live in is the pre-existing `if/elif/else` verbatim — all 51 existing self-test cases pass untouched. 1. **neither routed nor declared** → today's message, unchanged; and now the only one carrying the blanket “⛔ Do not resolve generated files textually” line, which is true of a set that by construction holds no generated file. 2. **routed and conflicted (MIXED)** → today's message, unchanged (objectstack-ai#14733's fix, and it is correct). 3. **declared in `NOT_DRIVER_MANAGED`** → new, one reading per path. Real output, from the new fixture: ```text ✗ merge stopped on conflicts, and some are in files a generator writes which are deliberately NOT driver-managed — ⛔ neither the non-generated rule nor the MIXED one governs those, so each is named below with its own: ⚠ generated/marked.txt — GENERATED IN MARKED REGIONS, deliberately NOT driver-managed. ⛔ Do NOT hand-merge the generated regions — resolve them by REGENERATION: take either side to reach a committable state, commit the merge (step 3), then run its generator and commit that as its own commit: pnpm gen:fixture-marked ⚠ THE TWO SIDES DIFFER OUTSIDE THE GENERATED REGIONS (4 line(s)). Taking a side DROPS the other side's hand-written text there — silently, and with every gate green: a `check:` on this file proves it equals its generated sources and is no witness for the prose. ⛔ Carry those lines over BEFORE you regenerate. ⚠ generated/regions-only.txt — GENERATED IN MARKED REGIONS, deliberately NOT driver-managed. ⛔ Do NOT hand-merge the generated regions — resolve them by REGENERATION: take either side to reach a committable state, commit the merge (step 3), then run its generator and commit that as its own commit: pnpm gen:fixture-regions ✓ the two sides are identical outside the generated regions, so taking either side drops no hand-written text. ⚠ ledgers/whole.json — generator-touched and deliberately NOT driver-managed. It carries no generated-region markers, so there is no half a regeneration would restore: resolve it BY HAND (semantic merge, both intents stack). ⛔ Do NOT regenerate it as part of this merge — the ledger keeps the driver off it because a mid-merge recompute describes the half-merged tree; its `why` in scripts/regen-artifacts.mjs is the authority on what it may be regenerated from, and when. non-generated (resolve by hand — semantic merge, both intents stack): build/ignored.json src/plain.txt Resolve every class above by ITS OWN rule, then rerun this script to redo the generated-artifact half. ``` **The addendum's caveat is answered, not delegated.** Comment `5654282996` asked for the check the tooling never makes, and the objectstack-ai#18062 transplant `5654438150` supplied its live cost: on PR objectstack-ai#17835 `registry.ts` was resolved take-a-side-and-regenerate, and `step18.conversionIds` / `step18.rationale` — hand-authored regions **outside** the markers — were dropped silently, so a 17→18 hop stopped applying while a 115-family gate sweep stayed green. The script now reads both sides out of the index it already holds (`:2:` ours, `:3:` theirs), strips the generated regions from each, and reports whether the remainders differ, with a line count. The clean case prints its own `✓`, so the finding is falsifiable rather than decorative. ## How the script learns generatedness `NOT_DRIVER_MANAGED` is read **at run time** from `scripts/regen-artifacts.mjs` — one `node --input-type=module -e` call importing the module through `pathToFileURL` — for the same reason `.gitattributes` is read at run time. ⛔ No hard-coded path list. The regeneration command is built by the module's own `ownerRunCommand`, never assembled in the shell, so the string this script prints stays the command the `pre-commit` gate spawns. Three things the reader is deliberate about: - **`untracked: true` rows are dropped.** They are gitignored build output git never merges; the day one becomes tracked, `git-merge-regen.mjs --self-test` refuses, so this skip hides nothing. Pinned: a tracked file at such a path gets no class-3 reading. - **An unreadable ledger is loud.** A silent empty list would restore the defect with the evidence removed, so the run says the ledger could not be read and makes no class-3 claim it cannot support. Pinned in case 9d. - **One `git diff` per ledger row, and ⛔ never with an empty pathspec** — `git diff --diff-filter=U --` with no pathspec matches *everything*, which would promote every conflict into class 3. An empty ledger runs no `git diff` at all. ## The class-3 fixture (P4's gap), and the discriminating reading `--self-test` gains `st_fixture_ndm_conflict`, a synthetic repo whose conflicts are all unrouted and whose ledger declares four of the five. It carries every reading class 3 has to make in one run, including both halves of the card's own dark-control warning — *an unlisted path reads exactly like a non-generated one from the routing side alone*: | fixture path | declared? | markers? | expected class | |:--|:--|:--|:--| | `generated/marked.txt` | yes | yes, sides differ **outside** them | 3 — regenerate + the carry-over finding | | `generated/regions-only.txt` | yes | yes, sides differ only **inside** | 3 — regenerate, `✓` no prose at stake (the firing control) | | `ledgers/whole.json` | yes | no | 3 — ⛔ do **not** regenerate in this merge | | `build/ignored.json` | yes, `untracked` | no | 1 — the row is dropped | | `src/plain.txt` | ⛔ **no** | no | 1 — today's message, unchanged | Two mutations keep the new cases falsifiable, in the style cases 6b and 8b already use. **9b** empties the ledger read and the whole set collapses back into class 1: `GENERATED IN MARKED REGIONS` ⇒ 0, `conflicts in NON-generated files` ⇒ 1, `Do not resolve generated files textually` ⇒ 1 — the reported defect, reproduced on demand. **9c** pins the real ledger rather than the fixture that models it: the module still declares `packages/spec/src/migrations/registry.ts`, still records `gen:migration-registry` for it, a fabricated path is absent (the control), and `git check-attr` still reads `unspecified` for it — so routing it or dropping its entry reddens here instead of silently reverting the label. ## Verification ```text ✓ os-regen-merge self-test: all cases pass. ``` 79 cases, at `1f8f50dd9`: the 51 that existed before, unchanged and unweakened (no case deleted, no expectation loosened), plus 28 new ones. Before the change the same file ran 51/51. `node scripts/pm/dispatch-gates.mjs --commands scripts/pm/os-regen-merge.sh` derived 26 families; all 26 ran, all exited 0, and `--ran` reconciles: ```text Run reconciliation — 26 derived, 26 run, 0 NOT-MEASURED, 0 UNRUN. ✓ dispatch-gates --ran: 26 derived famil(ies) accounted for — 26 run, 0 NOT-MEASURED ``` Named in that set and worth quoting: `pnpm check:bash32-floor`, `pnpm check:nul-bytes`, `pnpm check:parse-guard`, `pnpm check:entry-guard`, `pnpm check:pnpm-filter-targets`, `node scripts/check-self-test-wired.mjs`, `node scripts/check-scripts-symbol-anchors.mjs` — each exit 0. ⛔ **shellcheck is not a family here**: it is installed nowhere in this container and nothing in `.github/workflows/` or `package.json` invokes it, so there was no shell-escape residue check to derive. `bash -n` parses clean, as do both mutated copies the self-test builds. `pnpm lint` is CI's run, not this PR's, and the narrowing is measured rather than asserted: ① the population read from eslint's own config — every `files` glob in `eslint.config.mjs` is `**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}` or a narrower JS/TS subset, and no shell extension appears in any of them; ② the file count from `--format json` — this PR's one changed path returns `errorCount: 0` with `"File ignored because no matching configuration was supplied."`, i.e. it contributes **zero** files to the linted population; ③ invariance for untouched files — `eslint.config.mjs:327` records that the repo *“never enables type-aware linting (no `parserOptions.project`, no typed `@typescript-eslint` rules) for ANY file”*, so no file this diff does not touch can change verdict. ## Changeset ⛔ None owed, and the mechanism is the label rather than a path rule: `changeset-check` in `pr-automation.yml` counts **added** `.changeset/*.md` files and errors when the count is zero unless the PR carries `skip-changeset` (or is the changesets release PR). There is no path-based exemption in the gate, so the label is the declaration. `scripts/pm/os-regen-merge.sh` ships in no package's `files[]` — it is a PM-loop tool run by hand, invoked by no workflow — so nothing published moves and `skip-changeset` is the correct declaration. ## Acceptance notes Out of scope, noted and ⛔ not filed: - **objectstack-ai#8360** (open, `pm:on-hold`) — *“`migrations/registry.ts` still text-merges”*. Adjacent and named by triage as possibly making this moot: that card is about the file conflicting **at all**, this one about what the script says when it does. Whoever takes objectstack-ai#8360 lands on a path this PR's class-3 reading already covers; nothing here blocks or pre-empts it. Successor: objectstack-ai#8360's implementer. - **objectstack-ai#17602** (open, p1) — the driver exiting 0 while discarding one side. Same family (*“a zero from this tooling is not evidence”*), different file (`scripts/git-merge-regen.mjs`). Untouched here. Successor: objectstack-ai#17602's implementer. - `content/docs/permissions/tenant-audit-census.mdx` carries a `BEGIN GENERATED:` region and is declared only through the `content/docs/permissions/**` directory entry, so it now reads as class 3 — correctly, since its region is regenerated by `scripts/tenant-audit-census.mjs` and guarded by `check-tenant-audit-census.mjs`. Noted because it is the one class-3 path the card does not name. Successor: none; no change is owed. `Clause-②: no` --- _Generated by [Claude Code](https://claude.ai/code/session_01DAcomhvR9kKizeYgg89Vo8)_ Co-authored-by: Claude <noreply@anthropic.com>
Fixes #16929
Executes Ruling A — director seat, decision batch #121 item 2, comment
5644017943(2026-09-12), carrying the maintainer's 「同意」. Nothing here re-opens a question that ruling settled; alternatives B / C / E are not revisited.Clause-②: no — this is a removal / narrowing. Nothing is widened, so no
needs:contract-review.PR #17401's landed half (the two
guidanceprescriptions stopping naming the key) stands and is not redone: both prescriptions onorigin/mainalready omit it, and this branch leaves their text alone.The six ruled items, one by one
page.zod.ts:assignedProfilesremoved;profiles/assignedTobecome refusals naming the permission-set route; the two guidance strings rewrittenpage.form.tshelpText and its four locale bundles removedmajorchangeset + an ADR-0087 semantic migration entry; key stripped onmigrate meta --storedwith a structured TODOmajorgrade is refused by a standing repo-wide gate — see One ruled item the tree refusesClause-②: noItem 1 — route correction: a
retiredKey()tombstone, not a bare shape deletionThe retirement playbook offers two routes and keys the choice on whether the schema is strict:
retiredKey()for a non-strict schema, delete-plus-guidancefor a strict one.PageSchemais astrictObject, so the first attempt took the strict route — and the build refused it:scripts/build-schemas.tscheck (a) is fatal for any key that leaves an emitting def, strictness notwithstanding, and check (c) then ratchets a baseline deletion against the merge base on one of three proofs — aged-out tombstone, def unreachable from the metadata-type roots, or whole def gone.ui/Pageis reachable from thepageroot and keeps emitting, so none holds. The route the tree actually permits here is the tombstone, which is also what the siblingview.pageNameretirement took two days ago.This is not a softening of the ruling. The key is unwritable:
tsctypes itnever, and a value reaching a parse raises the prescription. It simply stays in the walked shape, which is why its liveness row stays (asdead) and why the authorable-surface baseline marks it[RETIRED]instead of losing the line.Item 3 — one of the three false records is not where the ruling says it is
Every
path:linewas re-derived by sentence rather than trusted. Two of the three resolved as written; the second did not.packages/spec/liveness/page.json—liveciting a non-existent objectui bridgepackages/spec/liveness/view.json:125— the "page audience gate" justificationgit grepover that file finds zero hits foraudience,assignedProfilesorpage audience(lit control:pageNamereads 3 lines in the same file; dark control 0). The #17063pageNameretirement rewrote that row on 2026-09-10 and the justification left with it. The same assertion is live atpackages/spec/src/api/protocol.zod.ts(SearchAllPageHitSchema's TSDoc) — that is the one corrected herepackages/metadata-protocol/src/protocol.ts— "enforced at page render"So the count is still three, and all three assertions are gone; one of them lives at a different address than the ruling recorded.
Before → after, and what makes the new text true.
packages/spec/liveness/page.json— wasstatus: "live", note: "profile-scoped page audience; objectui bridges it (react/src/spec-bridge/bridges/page.ts) to PageLayout.assignedProfiles." Nowstatus: "dead"with averifiedAtand a note recording that the cited path does not exist in objectui (nor does anyspec-bridgedirectory), while two sibling objectui citations in the same file resolve. True because the key is now a tombstone and the ledger's own route table says a tombstoned key keeps its row with adeadverdict.packages/spec/src/api/protocol.zod.ts— was "where the page's own audience gate (assignedProfiles) applies unchanged". Now states that a page has no audience gate of its own, that the key which read as one was removed precisely because nothing enforced it, and that what protects a page is the permission sets on the data it shows. True because the key no longer exists and never had a reader.packages/metadata-protocol/src/protocol.ts— was "is enforced where it is enforced now, at page render". Now states the opposite and keeps the delegation posture the sweep rests on, which never depended on the key. True by the cross-repo measurement the card and triage both took.packages/metadata-protocol/**isdomain:engine's lane. It is here only because ruling item 3 puts all three records in one PR, and exactly one sentence is touched.Measurement
The removal is real, and it reaches the built artifact
Probed against the built
packages/spec/dist/ui/index.mjsbefore and after, same script both times.Before (
origin/maincontent, built):After (this branch, rebuilt):
That is the actual refusal text, not a claim that one exists. Note the refusal moved channel:
unrecognized_keysat the page →invalid_typelocated at["assignedProfiles"], which is what az.never()tombstone produces.The alias refusals point somewhere true — read, not inherited
A previous round on this card asserted that an alias table runs only from the
unrecognized_keyspath. I re-read the source rather than inherit it.packages/spec/src/shared/strict-object.ts's own docblock puts it in terms — "aliases… is consulted BEFORE the distance fallback" insidestrictUnknownKeyError— andshared/alias-integrity.test.tsstates the mechanism as the premise of the gate it implements: "an alias only ever runs from theunrecognized_keyspath, so a key the shape declares can never reach it."The before-probe is the direct evidence: rows B and C above are
REFUSEDwithcode=unrecognized_keysbefore any change. SoprofilesandassignedTowere never in the accept set, the alias only decorated the rejection, and deleting or repointing those entries narrows nothing — same code, same path, different text. My own reading agrees with the earlier round's.A second consequence made the entries impossible to keep:
alias-integrity.test.tsasserts that an alias's target is a key the shape accepts. Once the key is a tombstone,profiles: 'assignedProfiles'would point at a key the schema cannot accept — the ledger's finding-7 shape. They had to become guidance.The migration entry actually fires
Driven over a stored page carrying the key, against the built artifact:
The key goes on both seams and the structured TODO appears. The strip is deliberately paired with a D3 semantic entry rather than left to read as "handled": which permission set a given profile name corresponds to is a judgement no walker can derive.
Ablation — the three new pins, RED before and GREEN after
One mutation leg restores both halves of the fix (the live key, and the two alias entries in place of the guidance ones).
Restore is proven by the blob hash against the HEAD blob and by an empty
git diff HEAD+ emptygit status --porcelain, never by an exit code. No rebuild leg is needed and none is claimed:page.test.tsimports./page.zod— a relative source path inside the same package — so this ablation never resolves throughdist.alias lineoccurrence count printed empty because thegrep -cwas mis-quoted inside a double-quoted$(...). The mutation is nonetheless established by the two counts that did fire and by the blob-hash change; a clean re-grep on the shipped file reads 0 alias lines (lit controlaliases: {= 6, dark control = 0).Changeset — it reaches a published
dist, with both controlsBuilt first, then measured (
npm pack --dry-run --jsonbefore a build readsdistas empty — that trap is avoided).dist/files and indist/index.d.ts(3 hits), so a consumer'stscand runtime both see it.src/ui/page.zod.tsis itself in the packed list.src/ui/page.test.tsandscripts/build-schemas.tsare NOT in the packed list (2012 files packed; 216dist/, 201src/, 38liveness/).//comment inpage.zod.tsreads 18 hits indist/.packages/spec's tsup build does not strip comments, so for this package a comment inside a shipped module is published text. Useful, and worth knowing before writing one.packages/metadata-protocolcorrectly carries no changeset entry: itsfilesis["dist","README.md","CHANGELOG.md"], the corrected sentence is an inline body comment, and it reads 0 files in that package'sdist(lit controlCLOSURE_CONTEXT_KEY_BY_TYPE= 2, so the instrument fired).majorgradeRuling item 4 fixes the changeset at
major. The changeset in this PR ismajor, as ruled. A standing repo-wide gate refuses it:So the ruling can be executed literally — the gate names its own escape — but the escape asserts "a whole-stack major release is genuinely intended", and that is a release-shaping claim well beyond this card:
17.xto18.0.0;scripts/sync-protocol-version.mjskeysPROTOCOL_VERSIONoff the spec package major, so the bump also flips the handshake to18and activates the 24 othertoMajor: 18conversions already waiting in the registry.⛔ I have therefore not applied
allow-major, and I have not silently regraded the changeset tominor. The grade stands as ruled and the gate stands red, with its reading recorded here. The remedy is one line and it belongs to the seat or the maintainer:major⇒ add theallow-majorlabel, and this PR is the one that cuts18.0.0; orminor⇒ the launch-window convention, which the gate's own prose says is carried instead by the BREAKING banner and the ADR-0087 disposition — both of which this changeset already has. It is also what the siblingview-page-mountretirement (an identical-shape breaking removal, two days ago) did.The hot registry file — what was taken, and against which tip
packages/spec/src/migrations/registry.tsis contended by #17792, #17638 and #17635. No entry number was taken, because there are none. The contended regions of that file are generated:src/migrations/entries/holds one file per entry, filename derived from the id, no index, concatenated bygen:migration-registryand sorted by id (entries/README.mdis the authority). This PR adds two such files and never edits between the markers:entries/retired-keys/18.ui__Page__assignedProfiles.ts→RETIRED_KEYS_BY_MAJOR[18]entries/semantic/18.page-assigned-profiles-audience-to-permission-set.ts→MIGRATIONS_BY_MAJOR[18].semanticTwo hand-edited lines remain, both appends at the tail and neither renumbering anything:
step18.conversionIdsgains'page-assigned-profiles-removed'after'view-page-mount-removed', andstep18.rationalegains a paragraph. ⛔ No other PR's entry is renumbered or reordered.The merge, and what it actually collided with.
origin/mainwas merged immediately before opening this PR: merge commit8774a8c115, parents3a1be112ff(this branch) +c1078a5591(origin/mainat that moment). The collision was real but it was not a number — it wasstep18.rationale, where #17260's landedobject-kanban.quickAddretirement and this card had each appended a paragraph to the same prose field. Resolved semantically, both intents kept, main's paragraph first:"ui/ObjectKanbanProps:quickAdd [RETIRED]"baseline marker — the os-regen driver's documented exit-0-while-dropping-a-side behaviour.scripts/pm/os-regen-merge.sh's order was followed (merge committed first, regeneration as its own commit,pre-commitdeferral discharged), and the regeneration put that marker back. That restoration is visible as its own commit;scheduledcache-warmup strategy — the cron it selected left in this same major (ADR-0049) #17638 or feat(spec): refuse a duration key whose JSDoc names a unit its describe does not #17635 was renumbered, reordered or touched.Everything in Verification below was re-run on the merged tree, at
626ca34833.⭐ T1 of #17618 — known false positive, recorded in advance
This PR declares
Clause-②: no, which is the only condition under which #17618's T1 leg fires, and T1 reads a re-declared key line as a new key (three false positives to date, most recently #17796 on a.describe()change). This diff re-declaresassignedProfileson its existing key line and rewrites alias/guidance entries on existing lines, so it is squarely in T1's blast radius. If T1 reds on a line this PR did not add, that is the false positive — the reading goes here, and ⛔nois not flipped toyesto clear a gate: the declared value is the criterion, not the diff shape.Verification
Exit codes captured before any pipe. Heavy runs went through
scripts/pm/os-verify-lock.shwithOS_VERIFY_LOCK_SLOT=issue-16929; the verdict quoted is the wrapper's ownVERDICTline, or a per-partecho "$?"marker where parts were sequenced.All of the following at
626ca34833, the merged head.pnpm --filter @objectstack/spec buildVERDICT command-exit 0pnpm --filter @objectstack/spec check:generatedcontent/docs/references/**andliveness/state-counts.md— regenerated with--fix)test+typecheck+ platform-objectstest+ metadata-protocoltypecheck, joined with&&so one verdict covers all fourVERDICT command-exit 0— spec: Test Files 471 passed (471), Tests 13375 passed (13375); platform-objects: 39 files, 561 testsnpx eslint . --no-inline-config --format json— the full repo union, no narrowing claimedESLINT_EXIT=0— 6647 files received per--format json, 0 errors, 0 warningsnode scripts/pm/dispatch-gates.mjs --ran … --repo objectstack-ai/objectstackEXIT=0— 114 derived, 112 run, 2 NOT-MEASURED, 0 UNRUNnode scripts/check-i18n-bundles.mjs --writeEXIT=3= PREREQUISITE NOT MET = NOT MEASURED (the built CLI closure was absent); the closure was built (VERDICT command-exit 0, 57 tasks) and the re-run isEXIT=0, all nine bundles regeneratedpnpm check:nul-bytesEXIT=0— 8463 files scanned, no raw control bytes. Own sweep over the 20 changed paths withgrep -naPfinds none; lit control on an injected byte firesnode scripts/check-changeset-no-major.mjs --base origin/mainEXIT=1— deliberate, see aboveThe two NOT-MEASURED families both exited 3 = PREREQUISITE NOT MET, which is neither a pass nor a finding, and neither is counted green here:
pnpm check:dual-build-cjs-loads— reads built output for ten packages this worktree never built (@objectstack/studio,client-react, four connectors, …);pnpm check:type-check-debt— wantsturbo run build --filter='./packages/*' --filter='./packages/*/*'first, and its own text says ⛔ no ledger number may be raised on a run that measured nothing.CI's Build Core supplies both. Two families that first read red on a stale build were re-run after rebuilding and are green:
check:skill-examples(exit 0, 258 prose examples across 3 surfaces — it had refused on an unbuiltpackages/client-react/dist, which was then built) andcheck:react-declaration-parity, run as CI runs it withMANIFEST="$PWD/sdui.manifest.json" … --baseline react-declaration-parity.baseline.json --strict(exit 0, "no new DECLARATION divergence vs accepted baseline") — its own refusal text says a complete local run is available from the checked-in root manifest and ⛔ must not be reported as NOT MEASURED.One earlier reading is withdrawn rather than quietly dropped:
pnpm check:query-options-erasurefirst readexit 2. That run happened whilesrc/migrations/registry.tsbriefly held a merge-resolution syntax error, and the gate parses that very file. Re-run on the fixed tree it isexit 0— "ratchet holds: 67 unswept non-test site(s) in 17 file(s), none new".Regenerated artifacts, each reviewed rather than waved through:
authorable-surface/ui.json—"ui/Page:assignedProfiles"→"ui/Page:assignedProfiles [RETIRED]"liveness/state-counts.md—page23 live → 22 live + 1 dead (total 24 unchanged); repo total 850→849 live, 93→94 deadcontent/docs/references/ui/page.mdx— the row's type becomesneverand carries the[REMOVED]prescription*.metadata-forms.generated.tsbundles lose theassignedProfilesblock (zh-CN 「指定配置文件 / 此页面对哪些 Profile 可用」, ja-JP「割り当てプロファイル」, es-ES "Perfiles asignados", en)Commit messages were swept for a card relation and a model identifier, each stem counted separately:
fix/fixes/fixed/close/closes/closed/resolve/resolves/resolved/part of/refsand#+digits all read 0;Opus/Sonnet/Haikuread 0. Lit controlClaudereads 8, so the sweep reaches. The onlyclaude-/anthropichits are the mandated trailer pair.验收备注
docs/adr/0010-nl-to-flow-authoring.mdmentionsassignedProfilesin a 2026-era open question. Untouched on purpose:docs/adr/**is a governed surface, and one path hit would make this whole diff governed and unmergeable by the queue. Noted, not filed.docs/audits/2026-06-pageschema-property-liveness.mdanddocs/audits/2026-06-security-identity-property-liveness.mdboth name the key. Untouched: they are dated audit records, and editing them would falsify the record rather than correct it..changeset/page-guidance-stops-prescribing-assignedprofiles.md(PR fix(spec): PageSchema's rejection guidance stops prescribingassignedProfilesas a page gate #17401's, still pending) states "assignedProfilesremains an authorable key with its declaration untouched". Accurate about that PR; superseded by this one inside the same unreleased window. This changeset says so rather than editing another PR's.Authored by the
domain:specexecution seat'sos-devround, sessionsession_01MkQhmuuJAVDjmeWNixwDDH, on branchclaude/issue-16929-assignedprofiles-removalat626ca34833.Generated by Claude Code